﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Xml;

namespace Notoric.Model.Collections
{
    /// <summary>
    /// Represents a dynamic string path collection that is backed-up by an XmlElement
    /// and provides notifications when items get added, removed, or when the whole list is refreshed.
    /// </summary>
    public class PathCollection : IEnumerable<PathElement>, INotifyCollectionChanged, INotifyPropertyChanged
    {
        #region Constructors

        /// <summary>
        /// Creates a new <see cref="PathCollection"/> instance.
        /// </summary>
        /// <param name="collectionElement">
        /// <para>
        /// Type: <see cref="XmlElement"/>
        /// </para>
        /// <para>
        /// The root collection element.<br/>
        /// If the element has child elements, the collection attempts to recreate
        /// its content from it.
        /// </para>
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="collectionElement"/> is <see langword="null"/>.
        /// </exception>
        public PathCollection(XmlElement collectionElement)
        {
            if (collectionElement == null)
                throw new ArgumentNullException("collectionElement");

            this.collectionElement = collectionElement;
            pathElements = new ObservableCollection<PathElement>();
            pathElements.CollectionChanged += new EventHandler<NotifyCollectionChangedEventArgs>(pathElements_CollectionChanged);
            pathElements.PropertyChanged += new PropertyChangedEventHandler(pathElements_PropertyChanged);

            if (collectionElement.HasChildNodes)
                RecreateCollection();
        }       

        #endregion

        #region IEnumerable<PathElement> Members

        /// <inheritdoc/>
        public IEnumerator<PathElement> GetEnumerator()
        {
            return pathElements.GetEnumerator();
        }

        #endregion

        #region IEnumerable Members

        /// <inheritdoc/>
        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
        {
            return pathElements.GetEnumerator();
        }

        #endregion

        #region INotifyCollectionChanged Members

        /// <inheritdoc/>
        public event EventHandler<NotifyCollectionChangedEventArgs> CollectionChanged;

        #endregion

        #region INotifyPropertyChanged Members

        /// <inheritdoc/>
        public event PropertyChangedEventHandler PropertyChanged;

        #endregion

        #region Event Handlers

        /// <summary>
        /// Occurs when the value of some property of the <see cref="pathElements"/> collection has changed.
        /// </summary>
        /// <remarks>
        /// <b>Operation:</b><br/>
        /// The event is resent via <see cref="PropertyChanged"/>.
        /// </remarks>
        /// <param name="sender">Object that has sent the event</param>
        /// <param name="e">Event data</param>
        private void pathElements_PropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            if (PropertyChanged != null)
                PropertyChanged(this, e);
        }

        /// <summary>
        /// Occurs when the <see cref="pathElements"/> collection has changed.
        /// </summary>
        /// <remarks>
        /// <b>Operation:</b><br/>
        /// The event is resent via <see cref="CollectionChanged"/>.
        /// </remarks>
        /// <param name="sender">Object that has sent the event</param>
        /// <param name="e">Event data</param>
        private void pathElements_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            if (CollectionChanged != null)
                CollectionChanged(this, e);
        }

        #endregion

        #region Private Methods

        /// <summary>
        /// Deletes all the collection items.
        /// </summary>
        private void ClearItems()
        {
            pathElements.Clear();

            foreach (XmlNode child in collectionElement.ChildNodes)
            {
                if (child.NodeType == XmlNodeType.Element)
                    collectionElement.RemoveChild(child);
            }
        }

        /// <summary>
        /// Inserts a new item at the specified index of the collection.
        /// </summary>
        /// <param name="index">Index</param>
        /// <param name="item">Item</param>
        private void InsertItem(int index, PathElement item)
        {
            if (index < pathElements.Count)
                collectionElement.InsertBefore(item.Element, collectionElement.ChildNodes[index]);
            else
                collectionElement.AppendChild(item.Element);

            pathElements.Insert(index, item);
        }

        /// <summary>
        /// Moves the item within the collection.
        /// </summary>
        /// <param name="oldIndex">Old index</param>
        /// <param name="newIndex">New index</param>
        private void MoveItem(int oldIndex, int newIndex)
        {
            var item = pathElements[oldIndex];

            RemoveItem(oldIndex);
            InsertItem(newIndex, item);
        }

        /// <summary>
        /// Loads the <see cref="pathElements"/> collection from the <see cref="collectionElement"/>.
        /// </summary>
        private void RecreateCollection()
        {
            foreach (XmlNode child in collectionElement.ChildNodes)
            {
                if (child is XmlElement)
                    pathElements.Add(new PathElement((XmlElement)child));
            }
        }

        /// <summary>
        /// Removes an item from the collection.
        /// </summary>
        /// <param name="index">Index</param>
        private void RemoveItem(int index)
        {
            var item = pathElements[index];
            pathElements.RemoveAt(index);
            collectionElement.RemoveChild(item.Element);
        }

        #endregion

        #region Internal Properties

        /// <summary>
        /// Gets a reference to the root XML element of the collection.
        /// </summary>
        internal XmlElement CollectionElement
        {
            get { return collectionElement; }
        }

        #endregion

        #region Public Methods

        /// <summary>
        /// Adds a new mapping to the end of the collection.
        /// </summary>
        /// <param name="virtualPath">
        /// <para>
        /// Type: <see cref="System.String"/>
        /// </para>
        /// <para>
        /// The virtual path that is being mapped.
        /// </para>
        /// </param>
        /// <param name="uri">
        /// <para>
        /// Type: <see cref="Uri"/>
        /// </para>
        /// <para>
        /// The part uri that the virtual path is mapped to.
        /// </para>
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="virtualPath"/> or <paramref name="uri"/> is <see langword="null"/>.
        /// </exception>
        /// <exception cref="ArgumentException">
        /// The collection already contains a mapping for <paramref name="virtualPath"/>.
        /// </exception>
        public void Add(string virtualPath, Uri uri)
        {
            if (virtualPath == null)
                throw new ArgumentNullException("virtualPath");
            if (uri == null)
                throw new ArgumentNullException("uri");
            if (this[virtualPath] != null)
                throw new ArgumentException("The virtual path " + virtualPath + " is already mapped!", "virtualPath");

            this[virtualPath] = uri;
        }

        /// <summary>
        /// Checks is the collection contains a mapping for a given virtual path.
        /// </summary>
        /// <param name="virtualPath">
        /// <para>
        /// Type: <see cref="System.String"/>
        /// </para>
        /// <para>
        /// The virtual path that is being checked.
        /// </para>
        /// </param>
        /// <returns>
        /// <para>
        /// Type: <see cref="System.Boolean"/>
        /// </para>
        /// <para>
        /// <see langword="true"/> if the collection contains a mapping for <paramref name="virtualPath"/>, <see langword="false"/> otherwise.
        /// </para>
        /// </returns>
        public bool Contains(string virtualPath)
        {
            if (virtualPath == null)
                throw new ArgumentNullException("virtualPath");

            return pathElements.FirstOrDefault(pe => pe.VirtualPath == virtualPath) != null;
        }

        /// <summary>
        /// Searches for a mapping of a given virtual path and returns
        /// the zero-based index of the element within the collection.
        /// </summary>
        /// <param name="virtualPath">
        /// <para>
        /// Type: <see cref="string"/>
        /// </para>
        /// <para>
        /// The virtual path being searched.
        /// </para>
        /// </param>
        /// <returns>
        /// <para>
        /// Type: <see cref="Int32"/>
        /// </para>
        /// <para>
        /// The zero-based index of the path element that maps the given
        /// virtual path or -1 if not found.
        /// </para>
        /// </returns>
        public int FindIndex(string virtualPath)
        {
            return pathElements.FindIndex(pe => pe.VirtualPath == virtualPath);
        }

        /// <summary>
        /// Searches for a mapping of a given virtual path and returns
        /// the zero-based index of the element within the collection.
        /// </summary>
        /// <param name="partUri">
        /// <para>
        /// Type: <see cref="Uri"/>
        /// </para>
        /// <para>
        /// The URI of the part being searched.
        /// </para>
        /// </param>
        /// <returns>
        /// <para>
        /// Type: <see cref="Int32"/>
        /// </para>
        /// <para>
        /// The zero-based index of the path element that maps the given
        /// part or -1 if not found.
        /// </para>
        /// </returns>
        public int FindIndex(Uri partUri)
        {
            return pathElements.FindIndex(pe => pe.URI == partUri);
        }

        /// <summary>
        /// Inserts a new mapping at the specified index in the collection.
        /// </summary>
        /// <param name="virtualPath">
        /// <para>
        /// Type: <see cref="System.String"/>
        /// </para>
        /// <para>
        /// The virtual path that is being mapped.
        /// </para>
        /// </param>
        /// <param name="uri">
        /// <para>
        /// Type: <see cref="Uri"/>
        /// </para>
        /// <para>
        /// The part uri that the virtual path is mapped to.
        /// </para>
        /// </param>
        /// <param name="index">
        /// <para>
        /// Type: <see cref="System.Int32"/>
        /// </para>
        /// <para>
        /// The zero-based index specifying the location where the new mapping should be inserted.
        /// </para>
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="virtualPath"/> or <paramref name="uri"/> is <see langword="null"/>.
        /// </exception>
        /// <exception cref="ArgumentException">
        /// The collection already contains a mapping for <paramref name="virtualPath"/>.
        /// </exception>
        /// <exception cref="ArgumentOutOfRangeException">
        /// The <paramref name="index"/> is less than 0 or greater than <see cref="Count"/>.
        /// </exception>
        public void Insert(string virtualPath, Uri uri, int index)
        {
            if (virtualPath == null)
                throw new ArgumentNullException("virtualPath");
            if (uri == null)
                throw new ArgumentNullException("uri");
            if (this[virtualPath] != null)
                throw new ArgumentException("The virtual path " + virtualPath + " is already mapped!", "virtualPath");
            if (index < 0 || index > pathElements.Count)
                throw new ArgumentOutOfRangeException("index");

            InsertItem(index, new PathElement(this, virtualPath, uri));
        }

        /// <summary>
        /// Moves a given item in the collection to a given index.
        /// </summary>
        /// <param name="virtualPath">
        /// <para>
        /// Type: <see cref="System.String"/>
        /// </para>
        /// <para>
        /// The virtual path whose mapping is being moved.
        /// </para>
        /// </param>
        /// <param name="newIndex">
        /// <para>
        /// Type: <see cref="System.Int32"/>
        /// </para>
        /// <para>
        /// The zero-based index specifying the new location of the item.
        /// </para>
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="virtualPath"/> is <see langword="null"/>.
        /// </exception>
        /// <exception cref="ArgumentException">
        /// The collection does not contain any mapping for <paramref name="virtualPath"/>.
        /// </exception>
        /// <exception cref="ArgumentOutOfRangeException">
        /// <paramref name="newIndex"/> is less than 0 or greater than <see cref="Count"/> - 1.
        /// </exception>
        public void Move(string virtualPath, int newIndex)
        {
            if (virtualPath == null)
                throw new ArgumentNullException("virtualPath");

            int oldIndex = -1;
            for (int i = 0; i < pathElements.Count; ++i)
            {
                if (pathElements[i].VirtualPath == virtualPath)
                {
                    oldIndex = i;
                    break;
                }
            }
            
            if (oldIndex == -1)
                throw new ArgumentException("The collection does not contain any mapping for " + virtualPath + "!", "virtualPath");

            Move(oldIndex, newIndex);
        }

        /// <summary>
        /// Moves an item within the collection.
        /// </summary>
        /// <param name="oldIndex">
        /// <para>
        /// Type: <see cref="System.Int32"/>
        /// </para>
        /// <para>
        /// The zero-based index specifying the location of the item to be moved.
        /// </para>
        /// </param>
        /// <param name="newIndex">
        /// <para>
        /// Type: <see cref="System.Int32"/>
        /// </para>
        /// <para>
        /// The zero-based index specifying the new location of the item.
        /// </para>
        /// </param>
        /// <exception cref="ArgumentOutOfRangeException">
        /// <paramref name="oldIndex"/> or <paramref name="newIndex"/> is less than 0 or greater than <see cref="Count"/> - 1.
        /// </exception>
        public void Move(int oldIndex, int newIndex)
        {
            if (oldIndex < 0 || oldIndex >= pathElements.Count)
                throw new ArgumentOutOfRangeException("oldIndex");
            if (newIndex < 0 || newIndex >= pathElements.Count)
                throw new ArgumentOutOfRangeException("newIndex");

            MoveItem(oldIndex, newIndex);
        }

        /// <summary>
        /// Removes a mapping from the collection.
        /// </summary>
        /// <param name="partUri">
        /// <para>
        /// Type: <see cref="Uri"/>
        /// </para>
        /// <para>
        /// The URI of the part whose mapping is being removed.
        /// </para>
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="partUri"/> is <see langword="null"/>.
        /// </exception>
        public void Remove(Uri partUri)
        {
            if (partUri == null)
                throw new ArgumentNullException("partUri");

            int index = -1;
            for (int i = 0; i < pathElements.Count; ++i)
            {
                if (pathElements[i].URI == partUri)
                {
                    index = i;
                    break;
                }
            }

            if (index != -1)
                RemoveItem(index);
        }

        /// <summary>
        /// Removes a mapping from the collection.
        /// </summary>
        /// <param name="virtualPath">
        /// <para>
        /// Type: <see cref="System.String"/>
        /// </para>
        /// <para>
        /// The virtual path whose mapping is being removed.
        /// </para>
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="virtualPath"/> is <see langword="null"/>.
        /// </exception>
        public void Remove(string virtualPath)
        {
            if (virtualPath == null)
                throw new ArgumentNullException("virtualPath");

            int index = -1;
            for (int i = 0; i < pathElements.Count; ++i)
            {
                if (pathElements[i].VirtualPath == virtualPath)
                {
                    index = i;
                    break;
                }
            }

            if (index != -1)
                RemoveItem(index);
        }

        /// <summary>
        /// Changes the virtual path to a part in the collection.
        /// </summary>
        /// <param name="oldVirtualPath">
        /// <para>
        /// Type: <see cref="System.String"/>
        /// </para>
        /// <para>
        /// The old virtual path to the part whose mapping is being changed.
        /// </para>
        /// </param>
        /// <param name="newVirtualPath">
        /// <para>
        /// Type: <see cref="System.String"/>
        /// </para>
        /// <para>
        /// The new virtual path to the part.
        /// </para>
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="oldVirtualPath"/> or <paramref name="newVirtualPath"/> is <see langword="null"/>.
        /// </exception>
        /// <exception cref="ArgumentException">
        /// The collection does not contain a mapping for <paramref name="oldVirtualPath"/>.<br/>
        /// -or-<br/>
        /// The collection already contains a mapping for <paramref name="newVirtualPath"/>.
        /// </exception>
        public void RenamePart(string oldVirtualPath, string newVirtualPath)
        {
            if (oldVirtualPath == null)
                throw new ArgumentNullException("oldVirtualPath");
            if (newVirtualPath == null)
                throw new ArgumentNullException("newVirtualPath");
            if (oldVirtualPath == newVirtualPath)
                return;
            if (Contains(newVirtualPath))
                throw new ArgumentException("The collection already contains a mapping for " + newVirtualPath + "!", "newVirtualPath");

            var pathElement = pathElements.FirstOrDefault(pe => pe.VirtualPath == oldVirtualPath);
            if (pathElement != null)
                pathElement.VirtualPath = newVirtualPath;
            else
                throw new ArgumentException("The collection does not contain a mapping for " + oldVirtualPath + "!", "oldVirtualPath");
        }

        /// <summary>
        /// Changes the virtual path to a part in the collection.
        /// </summary>
        /// <param name="partUri">
        /// <para>
        /// Type: <see cref="Uri"/>
        /// </para>
        /// <para>
        /// The URI of the part whose mapping is being changed.
        /// </para>
        /// </param>
        /// <param name="newVirtualPath">
        /// <para>
        /// Type: <see cref="System.String"/>
        /// </para>
        /// <para>
        /// The new virtual path to the part.
        /// </para>
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="partUri"/> or <paramref name="newVirtualPath"/> is <see langword="null"/>.
        /// </exception>
        /// <exception cref="ArgumentException">
        /// The collection does not contain a mapping for <paramref name="partUri"/>.<br/>
        /// -or-<br/>
        /// The collection already contains a mapping for <paramref name="newVirtualPath"/>.
        /// </exception>
        public void RenamePart(Uri partUri, string newVirtualPath)
        {
            if (partUri == null)
                throw new ArgumentNullException("partUri");
            if (newVirtualPath == null)
                throw new ArgumentNullException("newVirtualPath");
            if (this[newVirtualPath] != null && this[newVirtualPath] != partUri)
                throw new ArgumentException("The collection already contains a mapping for " + newVirtualPath + "!", "newVirtualPath");

            var pathElement = pathElements.FirstOrDefault(pe => pe.URI == partUri);
            if (pathElement != null)
                pathElement.VirtualPath = newVirtualPath;
            else
                throw new ArgumentException("The collection does not contain a mapping for " + partUri + "!", "partUri");
        }

        #endregion

        #region Public Properties

        /// <summary>
        /// Gets or sets the URI that a given virtual path is mapped to.
        /// </summary>
        /// <param name="virtualPath">
        /// <para>
        /// Type: <see cref="System.String"/>
        /// </para>
        /// <para>
        /// The virtual path the mapping of which is being searched.
        /// </para>
        /// </param>
        /// <returns>
        /// <para>
        /// Type: <see cref="Uri"/>
        /// </para>
        /// <para>
        /// The <see cref="Uri"/> that the <paramref name="virtualPath"/> is mapped to
        /// or <see langword="null"/> if the collection does not contain any mapping for <paramref name="virtualPath"/>.
        /// </para>
        /// </returns>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="virtualPath"/> is <see langword="null"/> or the property is being set and <paramref name="value"/> is <see langword="null"/>.
        /// </exception>
        public Uri this[string virtualPath]
        {
            get 
            {
                if (virtualPath == null)
                    throw new ArgumentNullException("virtualPath");

                var pathElement = pathElements.SingleOrDefault(pe => pe.VirtualPath == virtualPath);
                if (pathElement != null)
                    return pathElement.URI;
                else
                    return null;
            }
            set
            {
                if (virtualPath == null)
                    throw new ArgumentNullException("virtualPath");
                if (value == null)
                    throw new ArgumentNullException("value");

                var pathElement = pathElements.SingleOrDefault(pe => pe.VirtualPath == virtualPath);
                if (pathElement == null)
                {
                    pathElement = new PathElement(this, virtualPath, value);
                    InsertItem(pathElements.Count, pathElement);
                }
                else
                    pathElement.URI = value;
            }
        }

        /// <summary>
        /// Gets the virtual part that is mapped to a given URI.
        /// </summary>
        /// <param name="partUri">
        /// <para>
        /// Type: <see cref="System.Uri"/>
        /// </para>
        /// <para>
        /// The URI whose mapping is being searched.
        /// </para>
        /// </param>
        /// <returns>
        /// <para>
        /// Type: <see cref="String"/>
        /// </para>
        /// <para>
        /// The vritual path that is mapped to the given URI if found or <see langword="null"/>
        /// if no such mapping exists.
        /// </para>
        /// </returns>
        public string this[Uri partUri]
        {
            get 
            {
                var pathElement = pathElements.SingleOrDefault(pe => pe.URI == partUri);
                if (pathElement != null)
                    return pathElement.VirtualPath;
                else
                    return null;
            }
        }

        /// <summary>
        /// Gets the path element on a specified index.
        /// </summary>
        /// <param name="index">
        /// <para>
        /// Type: <see cref="Int32"/>
        /// </para>
        /// <para>
        /// The zero-based index of the requested element.
        /// </para>
        /// </param>
        /// <returns>
        /// <para>
        /// Type: <see cref="PathElement"/>
        /// </para>
        /// <para>
        /// The element at the specified index.
        /// </para>
        /// </returns>
        /// <exception cref="ArgumentOutOfRangeException">
        /// The <paramref name="index"/> is out of range.
        /// </exception>
        public PathElement this[int index]
        {
            get { return pathElements[index]; }
        }

        /// <summary>
        /// Gets the count of the items in the collection.
        /// </summary>
        public int Count
        {
            get { return pathElements.Count; }
        }

        #endregion

        #region Private Fields

        /// <summary>
        /// Holds a reference to the root collection element.
        /// </summary>
        private XmlElement collectionElement;
        /// <summary>
        /// Holds the items of this collection.
        /// </summary>
        private ObservableCollection<PathElement> pathElements;

        #endregion
    }
}
